Skip to content

Remote server support #1423

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open

Remote server support #1423

wants to merge 20 commits into from

Conversation

amirejaz
Copy link
Contributor

@amirejaz amirejaz commented Aug 14, 2025

Support Remote MCP Servers with OAuth/OIDC Authentication

Overview

This PR adds support for running remote MCP servers using the thv run --remote command, similar to how local MCP servers are run. Remote MCP servers are treated as workloads managed via runconfigs, enabling full functionality such as client configuration, tool filtering, and import/export.

Key Features

1. Remote Server Support

  • Command: thv run --remote <URL of MCP server>
  • Transport Types: Uses existing transport types (SSE, streamable-http) - no new "remote" transport type
  • Workload Management: Remote servers are managed as workloads via runconfigs
  • Status Management: Remote servers appear in thv list command

2. OAuth/OIDC Authentication

  • OAuth Flow: --remote-auth-client-id and --remote-auth-client-secret
  • Custom OAuth Endpoints: --remote-auth-authorize-url and --remote-auth-token-url
  • OIDC Discovery: Automatic discovery of OAuth endpoints via OIDC discovery documents
  • Registry Integration: OAuth configuration from registry when running registry-based servers

3. Registry Support

  • Remote Server Registry: Support for remote servers in the registry
  • OAuth Configuration: Registry can provide OAuth endpoints, scopes, and parameters
  • Conflict Resolution: Handles conflicts between local and remote servers with same name

Usage Examples

Basic Remote Server

# Run remote server with default transport
thv run --remote https://api.example.com/mcp/ --name example-server

# Run with specific transport
thv run --remote https://mcp.example.com --name example-server --transport streamable-http

OAuth/OIDC Authentication

# OAuth with client credentials (uses OIDC discovery)
thv run --remote https://api.example.com/mcp/ --name example-server \
  --remote-auth-client-id <client-id> \
  --remote-auth-client-secret <client-secret>

# Custom OAuth endpoints (bypasses OIDC discovery)
thv run --remote https://api.example.com/mcp/ --name example-server \
  --remote-auth-client-id <client-id> \
  --remote-auth-client-secret <client-secret> \
  --remote-auth-authorize-url https://example.com/oauth/authorize \
  --remote-auth-token-url https://example.com/oauth/token

Registry-Based Remote Servers

# Run from registry (uses registry OAuth configuration)
thv run example --name example-registry --remote-auth-client-id <client-id> --remote-auth-client-secret <client-secret>

# Registry provides OAuth endpoints automatically
thv run sample --name sample-registry --remote-auth-client-id <client-id> --remote-auth-client-secret <client-secret>

Server Management

# List all servers (including remote)
thv list

# Remove remote server (removes runconfig only)
thv rm example-server

# Stop remote server
thv stop example-server

Technical Implementation

Authentication Flow

  1. OIDC Discovery → Discover OAuth endpoints via .well-known/openid_configuration
  2. Provided OAuth Endpoints (user or registry) → Use explicit OAuth endpoints
  3. Manual Authentication Detection → Check server for auth requirements
  4. Clear Error Message → Guide user to provide OAuth endpoints or use registry

OIDC Discovery

  • Discovery Endpoints: .well-known/openid_configuration and .well-known/oauth-authorization-server
  • Automatic Endpoint Detection: Discovers authorization_endpoint and token_endpoint
  • Fallback Patterns: Common OAuth endpoint patterns if discovery fails

Registry Integration

  • Remote Server Metadata: RemoteServerMetadata struct with OAuth configuration
  • OAuth Config: Registry can provide authorize_url, token_url, scopes, oauth_params
  • Conflict Resolution: Comments out remote servers that conflict with local ones

Client Configuration

  • Automatic Setup: Remote servers are automatically added to client configurations
  • Proxy URLs: Clients connect to local proxy URLs (e.g., localhost:port)
  • Transport Support: Supports SSE and streamable-http transports

Configuration

Registry Schema

Remote servers are defined in pkg/registry/data/registry.json:

{
  "remote_servers": {
    "example": {
      "url": "https://api.example.com/mcp/",
      "transport": "streamable-http",
      "oauth_config": {
        "authorize_url": "https://example.com/oauth/authorize",
        "token_url": "https://example.com/oauth/token",
        "scopes": ["openid", "profile", "email"],
        "callback_port": 8666
      }
    }
  }
}

RunConfig Structure

type RemoteAuthConfig struct {
    EnableRemoteAuth bool
    ClientID         string
    ClientSecret     string
    AuthorizeURL     string
    TokenURL         string
    Scopes           []string
    OAuthParams      map[string]string
    CallbackPort     int
    Timeout          time.Duration
}

Limitations

  • No Dynamic Client Registration: Will be implemented in a separate PR
  • Manual OAuth Configuration: Users must provide OAuth endpoints or use registry
  • OIDC Discovery Only: Relies on OIDC discovery or explicit configuration

Next Steps

  • Dynamic Client Registration: Will be implemented in a separate PR to support automatic OAuth endpoint discovery and client registration
  • thv start, thv restart: Properly handle start and restart for remote mcp servers

@amirejaz amirejaz marked this pull request as draft August 14, 2025 10:53
@@ -4166,5 +4166,93 @@
"transport": "stdio"
}
},
"remote_servers": {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These remote servers are currently added only for testing purposes. I’m not suggesting adding them permanently to our registry.
If we decide to include them, I can ensure that all tools and associated metadata are correct.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no biggie.

@amirejaz amirejaz marked this pull request as ready for review August 19, 2025 16:25
@amirejaz amirejaz requested a review from JAORMX August 20, 2025 08:14
@@ -59,13 +60,25 @@ ToolHive supports four ways to run an MCP server:
Runs an MCP server using a previously exported configuration file.
5. Remote MCP server:
$ thv run --remote <URL> [--name <name>]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the --remote flag really needed? Can't we just determine that it should be a remote server by the fact that the entry is a URL?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can avoid --remote flag, it was added as the initial design for remote servers

cmd.Flags().StringVar(&config.RemoteAuthAuthorizeURL, "remote-auth-authorize-url", "",
"OAuth authorize URL for remote server authentication")
cmd.Flags().StringVar(&config.RemoteAuthTokenURL, "remote-auth-token-url", "",
"OAuth token URL for remote server authentication")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are shared with the proxy subcommand, right? Would it make sense to have a common function to set these?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, we can make them common.

// Handle image retrieval
imageURL, imageMetadata, err := handleImageRetrieval(ctx, serverOrImage, runFlags)
imageURL, imageMetadata, remoteServerMetadata, err := handleImageRetrieval(ctx, serverOrImage, runFlags)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it would make more sense to make this a two step process:

  • Determine whether a registry entry is remote or not
  • use the appropriate entry for the server... this will be a different function call depending on it being remote or not.

@@ -324,17 +397,29 @@ func buildRunnerConfig(
rt runtime.Deployer,
imageURL string,
imageMetadata *registry.ImageMetadata,
remoteServerMetadata *registry.RemoteServerMetadata,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about using the ServerMetadata interface instead?

https://github.com/stacklok/toolhive/blob/main/pkg/registry/types.go#L185

This would allow you to only pass one value here and from there you can determine whether it's a remote entry or not.

@@ -723,7 +723,7 @@ func (s *WorkloadRoutes) getWorkloadNamesFromRequest(ctx context.Context, req bu
// createWorkloadFromRequest creates a workload from a request
func (s *WorkloadRoutes) createWorkloadFromRequest(ctx context.Context, req *createRequest) (*runner.RunConfig, error) {
// Fetch or build the requested image
imageURL, imageMetadata, err := retriever.GetMCPServer(
imageURL, imageMetadata, _, err := retriever.GetMCPServer(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about returning ServerMetadata here instead?

@@ -0,0 +1,345 @@
// Package discovery provides authentication discovery utilities for detecting
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could have been in a separate PR. It's fine now, but if you do want to expedite this I'd suggest that still.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is basically the existing functionality, just refactored it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Dynamic client registration, I would create a separate PR

Timeout: 30 * time.Second,
}

return client.Do(newReq)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this HTTP client doesn't use authentication at all... how is this used? I'm a little confused by this tbh.

func (t *tracingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if t.p.isRemote {
// Use manual HTTP client instead of reverse proxy for remote URLs
resp, err := t.manualForward(req)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't using manual forwarding break dual communication channels like SSE?

// For remote servers, strip the /mcp path since they expect requests at the root
if p.isRemote && strings.HasPrefix(r.URL.Path, "/mcp") {
// Strip /mcp from the path for remote servers
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/mcp")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't that be a matter of not using /mcp when doing the request?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants